/* This file is part of swapper project
 *
 * Copyright (C) 2020 The Swapper Project Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.swapper.json.io;

import com.swapper.json.*;
import com.swapper.json.JsonSymbols;

import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.io.Writer;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;

/**
 * JSON text writer.
 */
public final class JsonWriter implements Flushable, Closeable {
  private final Writer writer;

  /**
   * Write a JSON document(text) JSON format string to output writer.
   *
   * @param out      the output writer.
   * @param document the JSON document(text).
   */
  public static void write(Writer out, JsonDocument document) {
    try {
      new JsonWriter(out).write(document);
    } catch (IOException e) {
      throw new JsonIOException(e);
    }
    try {
      out.flush();
    } catch (IOException e) {
      throw new JsonIOException(e);
    }
    try {
      out.close();
    } catch (IOException e) {
      throw new JsonIOException(e);
    }
  }

  /**
   * The constructor.
   *
   * @param out the output writer.
   */
  public JsonWriter(Writer out) {
    writer = Objects.requireNonNull(out);
  }


  /**
   * Write a JSON document(text) JSON format string to output writer.
   *
   * @param document the JSON document(text).
   * @throws IOException if an I/O error occurs.
   */
  public void write(JsonDocument document) throws IOException {
    if (document instanceof JsonArray) {
      writeArray((JsonArray) document);
    } else if (document instanceof JsonObject) {
      writeObject((JsonObject) document);
    } else {
      throw new IllegalStateException("JsonDocument must be JsonArray or JsonObject.");
    }
  }

  /**
   * Write a JSON value JSON format string to output writer.
   *
   * @param value the JSON value.
   * @throws IOException if an I/O error occurs.
   */
  private void writeValue(JsonValue value) throws IOException {
    if (value instanceof JsonArray) {
      writeArray((JsonArray) value);
    } else if (value instanceof JsonObject) {
      writeObject((JsonObject) value);
    } else {
      writer.append(value.toJson());
    }
  }

  /**
   * Write a JSON array JSON format string to output writer.
   *
   * @param array the JSON array.
   * @throws IOException if an I/O error occurs.
   */
  private void writeArray(JsonArray array) throws IOException {
    int iMax = array.size() - 1;
    if (iMax == -1) {
      writer.append(JsonSymbols.S_EMPTY_ARRAY);
      return;
    }
    writer.append(JsonSymbols.BEGIN_ARRAY);
    for (int i = 0; ; ++i) {
      writeValue(array.get(i));
      if (i == iMax) {
        writer.append(JsonSymbols.END_ARRAY);
        return;
      }
      writer.append(JsonSymbols.VALUE_SEPARATOR);
    }
  }

  /**
   * Write a JSON object JSON format string to output writer.
   *
   * @param object the JSON array.
   * @throws IOException if an I/O error occurs.
   */
  private void writeObject(JsonObject object) throws IOException {
    int iMax = object.size() - 1;
    if (iMax == -1) {
      writer.append(JsonSymbols.S_EMPTY_OBJECT);
      return;
    }
    Iterator<Map.Entry<String, JsonValue>> iterator = object.members().iterator();
    writer.append(JsonSymbols.BEGIN_OBJECT);
    for (int i = 0; ; ++i) {
      Map.Entry<String, JsonValue> member = iterator.next();
      writer.append(JsonSymbols.QUOTATION_MARK)
        .append(member.getKey())
        .append(JsonSymbols.QUOTATION_MARK)
        .append(JsonSymbols.NAME_SEPARATOR);
      writeValue(member.getValue());
      if (i == iMax) {
        writer.append(JsonSymbols.END_OBJECT);
        return;
      }
      writer.append(JsonSymbols.VALUE_SEPARATOR);
    }
  }

  /**
   * Flushes the writer.
   *
   * @throws IOException if an I/O error occurs.
   */
  @Override
  public void flush() throws IOException {
    writer.flush();
  }

  /**
   * Closes the writer, flushing it first.
   *
   * @throws IOException if an I/O error occurs.
   */
  @Override
  public void close() throws IOException {
    writer.close();
  }

  /**
   * Gets the hash code of the current instance.
   *
   * @return the hash code of the current instance.
   * @see Object#hashCode()
   */
  @Override
  public String toString() {
    return writer.toString();
  }
}
